package com.czk.bootlettuce.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * redis分布式锁可能会出现的问题
 * 1 没有设置过期时间导致死锁
 * 2 设置时间和值可能不是原子性的
 * 3 可能会导致线程误删其余线程的key
 * 4 删除和过期时间没有原子性，刚要删除时锁已经过期了->lua脚本解决
 *
 * @Author:ChenZhangKun
 * @Date: 2021/8/23 14:54
 */
@RestController
@RequestMapping("/lock")
public class DistributeLockController {
    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping("/set")
    public String lock() {
        String uuid = UUID.randomUUID().toString().replaceAll("-", "");

        // 获取锁
        // 设置过期时间的目的是防止出现异常无法释放锁
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);
        // 获取锁成功
        if (lock) {
            Object value = redisTemplate.opsForValue().get("num");
            // 是否为空
            if (value == null) {
                return "获取值为空";
            }
            int num = Integer.parseInt(value + "");
            // 加一
            redisTemplate.opsForValue().set("num", ++num);
            // 释放锁 当网络延迟或代码卡顿时可能会造成误删，即自己设置的锁时间过期，但是代码并没有执行结束而导致删除了其他线程设置的key
            // 解决方案，使用UUID
            // 拿到uuid进行比对
            String uuid_redis = (String) redisTemplate.opsForValue().get("lock");
            if (uuid_redis == uuid) {
                // 表示同一个线程设置的锁
                // 有一个问题，不具有原子性，此时可能lock刚刚过期，导致误->解决方案，使用lua脚本
                // 2. 释放锁 del
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                // 设置lua脚本返回的数据类型
                DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>();
                // 设置lua脚本的返回值为long
                defaultRedisScript.setResultType(Long.class);
                defaultRedisScript.setScriptText(script);
                // lua脚本执行
                redisTemplate.execute(defaultRedisScript, Arrays.asList("lock"), uuid);
                // 普通执行
                // redisTemplate.delete("lock");
            }
            return "测试成功";
        } else {
            try {
                Thread.sleep(3);
                // 重新获取锁
                lock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return "操作失败";
    }
}
